package org.lizar.controller;

import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.lizar.Web;
import org.lizar.config.Config;
import org.lizar.config.i18.I18Resource;
import org.lizar.controller.handler.JSONHandler;
import org.lizar.controller.handler.JSONPHandler;
import org.lizar.controller.handler.XMLHandler;
import org.lizar.controller.intercepter.After;
import org.lizar.controller.intercepter.Before;
import org.lizar.controller.intercepter.EventInterceptor;
import org.lizar.controller.intercepter.EventExceptionRecorder;
import org.lizar.controller.loader.EventLoader;
import org.lizar.controller.staticsrc.ContentType;
import org.lizar.controller.staticsrc.StaticResource;
import org.lizar.controller.template.TemplateSupport;
import org.lizar.exception.EventHandlingPathIsNotSupported;
import org.lizar.exception.LifecycleException;
import org.lizar.ioc.Cell;
import org.lizar.ioc.Container;
import org.lizar.ioc.Lifecycle;
import org.lizar.json.JList;
import org.lizar.json.JSON;
import org.lizar.log.Log;
import org.lizar.log.Logger;
import org.lizar.util.FileTool;
import org.lizar.util.StringHelper;

/**
 * <pre>
 * task Intercept and dispatch requests to proper event
 * 
 * Event handlers with unsupported handle path will not be put into event_handler_map and will not handle request.
 * 			Unsupported handle path(Event's handle_path field) :
 * 								1 who is not start with "/"
 * 								2 who contains illegal chars such as '.'
 * 								3 null and ""
 * 								4 multi-"/" such as "/xxx//user"
 * 
 * 			Intercepting steps : 
 * 								1 if a template exists and listens the file type, the handle the request 
 * 
 * 								2 if a event handler in minor_event_handler_map can catch the request then handle it.
 * 
 * 								3 if all of above fail, get handler from high_event_handler_map
 * 
 * 
 * 			How a request visit path match an event handler in high_event_handler_map:
 * 								Firstly eliminating postfix like ".json" or "/" of visit path,but visit path is "/" do nothing.
 * 								Secondly,get from high_event_handler_map, if not match, eliminate last "/xxx" from visit path
 * 								then get from high_event_handler_map, if matches return the event handler else eliminate last "/xxx" 
 * 								from visit path and search again until find proper event handler or visit path is null.
 * 
 * 
 * caution 1 Event's priority field is set to promote retrieve efficiency.
 * 
 * caution 2 If "/xxx" and "/xxx/*" both are events' handle path, "/xxx"'s priority is higher.
 * 			 if "/xxx/*" and "/xxx/user/*" both are events' handle path, "/xxx/user/*"'s priority is higher.
 * 
 * caution 3 If an event handler is configured as default event handler, its handle path doesn't work 
 * 				and t is not the top event handler any more now, if it was.
 * 
 * caution 4 "/*" can intercept all request but its priority is the lowest in system.
 * 
 * </pre>
 */
public class Controller implements Lifecycle {
	private Log								log						= Logger.newInstance(this.getClass());
	
	private static boolean					started					= false;
	
	/**
	 * system supported template
	 * 
	 * Example freemarker velocity
	 */
	public static TemplateSupport			template;
	
	/**
	 * 
	 * store event handler with high priority
	 * 
	 * <path,event>
	 * 
	 * Path is used to seek event handler an it is event's handle path without "/*" as end.
	 * If you want to intercept all request put <"/",event>
	 * 
	 */
	public static Map<String, Event>		high_event_handler_map;
	
	/**
	 * 
	 * store event handler with minor priority
	 * 
	 * <path,event>
	 * 
	 * Path is used to seek event handler an it is event's handle path without "/" as end.
	 * If handle path is "/" put <"/",event>
	 * 
	 */
	public static Map<String, Event>		minor_event_handler_map;
	
	/**
	 * <event_full_class_name,event>
	 * 
	 * all event handlers go here.
	 */
	public static Map<String, Event>		event_map;
	
	/**
	 * configured in lizar.json which is set to response.
	 */
	public static Map<String, Object>		global_vars;
	
	private static EventInterceptor			int_cpt;
	
	private static EventExceptionRecorder	er;
	
	private static Controller				controller;
	
	public static final String				EVENT_DISABLE_FILE		= "/WEB-INF/lizar/disable_event.json";
	
	public static String					encode_type;
	
	public static JList						disable_list;
	
	public static long						disable_file_last_modify;
	
	public static StaticResource			static_src;
	
	/**
	 * 
	 * URL /ADFA*
	 * 
	 * */
	public static final int					SEARCH_PRIORITY_HIGHT	= 7;
	
	/**
	 * URL /
	 * 
	 * */
	public static final int					SEARCH_PRIORITY_MINOR	= 3;
	
	public static Controller instance() {
		if (controller == null) {
			controller = new Controller();
		}
		return controller;
	}
	
	private Controller() {
		
	}
	
	private void reload_disable_map() {
		disable_list.clear();
		File f = new File(Config.path_filter(EVENT_DISABLE_FILE));
		if (!f.exists()) {
			disable_file_last_modify = f.lastModified();
			try {
				FileTool.write_to_file(disable_list.toString(), new File(Config.path_filter(EVENT_DISABLE_FILE)), "utf-8");
			} catch (IOException e) {
				log.error("an error with   Controller disable file, pls check", e);
			}
			for (Entry<String, Event> entry : event_map.entrySet()) {
				entry.getValue().enable = true;
			}
			return;
		}
		if (f.lastModified() != disable_file_last_modify) {
			JSON e = null;
			try {
				e = FileTool.read_json(f);
			} catch (Exception ex) {
				log.error("error format of Controller disable file, pls check the json grammar in " + EVENT_DISABLE_FILE);
				return;
			}
			if (e == null) {
				log.info(" controller disable file turns to be null, all the diable event will be enable.");
				for (Entry<String, Event> entry : event_map.entrySet()) {
					entry.getValue().enable = true;
				}
				disable_file_last_modify = f.lastModified();
				return;
			}
			if (!(e instanceof JList)) {
				log.error("Json of " + EVENT_DISABLE_FILE + " file must be an json array.");
				return;
			}
			JList list = (JList) e;
			for (Entry<String, Event> entry : event_map.entrySet()) {
				entry.getValue().enable = true;
			}
			for (Object o : list) {
				Event event = event_map.get(o.toString());
				if (event != null) {
					event.enable = false;
				}
			}
			disable_file_last_modify = f.lastModified();
		}
	}
	
	public void _check_disable_event() {
		reload_disable_map();
	}
	
	public static void _init_encode_type(String encode_type) {
		Controller.encode_type = encode_type;
		Event.encode_type = encode_type;
	}
	
	public void _reload_encode_type(String encode_type) {
		Controller.encode_type = Config.xpathgetStr("server_info.encode_type", "utf-8");
		Event.encode_type = Config.xpathgetStr("server_info.encode_type", "utf-8");
	}
	
	public void _check_vars(JSON vars) {
		if (vars != null) {
			global_vars.clear();
			for (Entry e : (Set<Entry>) vars.toMap().entrySet()) {
				global_vars.put(e.getKey().toString(), e.getValue());
			}
		}
	}
	
	/**
	 * Applying template config from config file lizar.json and 
	 * 
	 * this allow system apply and config template dynamically.
	 * 
	 */
	public void _check_template_setting() {
		String template_class = Config.xpathgetStr("template.class");
		if (StringHelper.isNull(template_class)) {
			template = null;
			return;
		}
		JSON params = Config.xpath_entity("template.params");
		JList listen = Config.xpath_list("template.listen");
		if (template == null) {
			_load_template_support(template_class, listen, params);
			return;
		}
		if (!template.getClass().getName().equals(template_class)) {
			_load_template_support(template_class, listen, params);
			return;
		}
		template.listen(listen);
		if (params != null && !params.toMap().isEmpty()) {
			try {
				template.init(Web.context, params);
			} catch (ServletException e) {
				log.warn("Template Support Class " + template_class + " init failed.", e);
			}
		}
	}
	
	private void _load_template_support(String template_class, JList listen, JSON params) {
		if (StringHelper.isNull(template_class))
			return;
		try {
			Class t = Class.forName(template_class);
			try {
				t.asSubclass(TemplateSupport.class);
			} catch (Exception e) {
				log.debug("Template Support Class " + template_class + " must extends com.lizar.web.controller.TemplateSupport", e);
				return;
			}
			template = (TemplateSupport) t.newInstance();
			template.init(Web.context, params);
			template.listen(listen);
			log.info("..............................................................................");
			log.info("\t\t\tUse template " + template_class + " with listen post fix " + template.listen());
			log.info("..............................................................................");
		} catch (Exception e) {
			log.warn("Template Support Class " + template_class + " init failed.", e);
		}
	}
	
	private void pre_handle() {
		for (Entry<String, Event> e : event_map.entrySet()) {
			_express_event_listener(e.getKey(), e.getValue());
		}
		
	}
	
	/**
	 * 
	 * global Event intercepter init
	 * 
	 * */
	private void _initgetInt_cpt() {
		for (Entry<String, Cell> entry : Container.getAll().entrySet()) {
			try {
				entry.getValue().getClass().asSubclass(EventInterceptor.class);
			} catch (Exception e) {
				continue;
			}
			int_cpt = (EventInterceptor) entry.getValue();
			log.info("..............................................................................");
			log.info("\t\t\tController Event will use interceptor " + int_cpt.getClass() + " to handle before and after event ");
			log.info("..............................................................................");
			return;
		}
		log.info("..............................................................................");
		log.info("\t\t\tController Event has not find any interceptor  to handle before and after event ");
		log.info("..............................................................................");
	}
	
	/**
	 * 
	 * global Exception Recorder
	 * 
	 * */
	private void _init_er() {
		for (Entry<String, Cell> entry : Container.getAll().entrySet()) {
			try {
				entry.getValue().getClass().asSubclass(EventExceptionRecorder.class);
			} catch (Exception e) {
				continue;
			}
			er = (EventExceptionRecorder) entry.getValue();
			log.info("..............................................................................");
			log.info("\t\t\tController Event will use exception interceptor " + er.getClass() + " to handle event exception ");
			log.info("..............................................................................");
			return;
		}
		log.info("..............................................................................");
		log.info("\t\t\tController Event has not find any exception interceptor  to handle event exception ");
		log.info("..............................................................................");
	}
	
	private void _init_tpl_vars() {
		global_vars = new HashMap<String, Object>();
		JSON vars = Config.xpath_entity("event.global_var");
		if (vars != null) {
			for (Entry e : (Set<Entry>) vars.toMap().entrySet()) {
				global_vars.put(e.getKey().toString(), e.getValue());
			}
		}
	}
	
	private void pre_init(HttpServletRequest request, HttpServletResponse response) throws UnsupportedEncodingException {
		request.setCharacterEncoding(encode_type);
		response.setCharacterEncoding(encode_type);
		pre_tpl_global_var(request);
		pre_locale_map(request);
	}
	
	private void pre_locale_map(HttpServletRequest request) {
		I18Resource res = Web.i18.get(request.getLocale().getLanguage());
		if (res != null)
			request.setAttribute(Web.i18.var, res.map);
	}
	
	private void pre_tpl_global_var(HttpServletRequest request) {
		if (global_vars != null) {
			for (Entry<String, Object> e : global_vars.entrySet()) {
				request.setAttribute(e.getKey(), e.getValue());
			}
		}
	}
	
	public static String level_zero(String path) {
		int i = path.indexOf("/", 1);
		if (i == -1)
			return path;
		return path.substring(0, i);
	}
	
	/**
	 * If event's priority is high there is two cases "/*" and "/mc/user/*"
	 * 									
	 * If event's priority is minor there is three cases  "/", /mc" and "/mc/"					
	 */
	private void translate_handling_path(Event e) {
		if (e.priority() == SEARCH_PRIORITY_HIGHT) {
			if (e.handling_path().length() > 2 && e.handling_path().endsWith("/*"))
				e.handling_path(e.handling_path().substring(0, e.handling_path().length() - 2));
			else
				e.handling_path(e.handling_path().substring(0, e.handling_path().length() - 1));
		} else if (e.priority() == SEARCH_PRIORITY_MINOR) {
			if (e.handling_path().equals("/"))
				e.handling_path("/");
			else if (e.handling_path().endsWith("/"))
				e.handling_path(e.handling_path().substring(0, e.handling_path().length() - 1));
			else
				e.handling_path(e.handling_path());
		}
	}
	
	/**
	 * @task1 set an event's priority
	 * @task2 set and an event's priority
	 * @task3 assign values to maps.
	 * 
	 * @caution1 if event.default_handling_path() is not supported, it will not be put into event_handler_map.
	 * 
	 * @param key event's full class name
	 * 
	 */
	private void _express_event_listener(String key, Event event) {
		event.handling_path(StringHelper.isNull(event.default_handling_path()) ? "" : event.default_handling_path().toLowerCase().trim());
		if (!event.handling_path().startsWith("/")) {
			event.handling_path("/" + event.handling_path());
		}
		event.priority(get_event_priority(event.handling_path().trim()));
		translate_handling_path(event);
		if (event.priority() == SEARCH_PRIORITY_HIGHT) {
			high_event_handler_map.put(event.handling_path(), event);
			log.info("High level event handler " + event.getClass().getName() + " with key " + key + " has been loaded to high_event_handler_map.");
		} else if (event.priority() == SEARCH_PRIORITY_MINOR) {
			minor_event_handler_map.put(event.handling_path(), event);
			log.info("Minor level event handler " + event.getClass().getName() + " with key " + key + " has been loaded to minor_event_handler_map.");
		}
		event.set_servlet_context(Web.context);
		event_map.put(key, event);
	}
	
	/**
	 * @returns 0 if request top event
	 * 
	 * @param visit_uri such as /search/blog.json
	 */
	public static int visit_path_level(String visit_uri) {
		if (StringHelper.isNull(visit_uri))
			return 0;
		visit_uri = visit_uri.substring(1, visit_uri.length());
		int i = 0;
		for (String s : visit_uri.split("/")) {
			if (StringHelper.isNotNull(s))
				i++;
		}
		return i;
	}
	
	/**
	 * <p>
	 * Get a event's priority according its handle path.
	 * 
	 * priority:
	 * 			high : if visit_uri is end with "/*" 
	 * 			minor : if visit_uri doesn't contain "*"
	 * 
	 * @caution visit_uri must start with "/".
	 * </p>
	 * 
	 * @param visit_uri event's handle_path
	 * 
	 * @throws EventHandlingPathIsNotSupported if visit_uri format is not supported.
	 */
	public static int get_event_priority(String visit_uri) {
		if (StringHelper.isNotNull(visit_uri)) {
			if (visit_uri.endsWith("/*"))
				return SEARCH_PRIORITY_HIGHT;
			if (visit_uri.indexOf("*") == -1)
				return SEARCH_PRIORITY_MINOR;
		}
		throw new EventHandlingPathIsNotSupported(visit_uri + " is currently not supported in  controller uri interceptor pattern");
	}
	
	private void _handle(Event handler, String path, EventLoader el) throws Exception {
		if (path.indexOf(".") == -1) {
			if (handler instanceof Before)
				((Before) handler).before(el);
			handler.handle(el);
			if (handler instanceof After)
				((After) handler).after(el);
		} else if (path.endsWith(".json")) {
			if (handler instanceof Before)
				((Before) handler).before(el);
			if (handler instanceof JSONHandler)
				((JSONHandler) handler).handle_json(el);
			if (handler instanceof After)
				((After) handler).after(el);
		} else if (path.endsWith(".xml")) {
			if (handler instanceof Before)
				((Before) handler).before(el);
			if (handler instanceof XMLHandler)
				((XMLHandler) handler).handle_xml(el);
			if (handler instanceof After)
				((After) handler).after(el);
		} else if (path.endsWith(".jsonp")) {
			if (handler instanceof Before)
				((Before) handler).before(el);
			if (handler instanceof JSONPHandler)
				((JSONPHandler) handler).handle_jsonp(el);
			if (handler instanceof After)
				((After) handler).after(el);
		} else {
			StaticResource.handle(el, path);
		}
		if (int_cpt != null && el.need_after) {
			int_cpt.after(el);
			el.need_after = false;
		}
	}
	
	public void handle_event(HttpServletRequest request, HttpServletResponse response, String path, String post_fix) throws ServletException, IOException {
		pre_init(request, response);
		boolean need_before = true;
		EventLoader el = (EventLoader) request.getAttribute("com.lizar.web.controller.eventloader");
		if (el == null) {
			el = new EventLoader(path, request, response);
			request.setAttribute("com.lizar.web.controller.eventloader", el);
		} else
			need_before = false;
		el.postfix(post_fix);
		el.lan = request.getLocale().getLanguage();
		if (int_cpt != null && need_before) {
			try {
				int_cpt.before(el);
			} catch (Exception e) {
				log.error(e);
				if (er != null)
					er.handle(el, e);
			}
		}
		if(!el.response().isCommitted()){
			try {
				handle_event(el);
			} catch (Exception e) {
				log.error(e.getMessage(), e);
				if (er != null) {
					er.handle(el, e);
				}
				if (!el.response().isCommitted()) {
					handle_500(el);
				}
			}
		}
	}
	
	public void handle_event(EventLoader el) throws Exception {
		if (StringHelper.isNotNull(el.postfix()) && template != null && template.listen().indexOf(el.postfix()) != -1) {
			template.handle(el.getRequestPath(), el);
			if (int_cpt != null && el.need_after) {
				int_cpt.after(el);
				el.need_after = false;
			}
			return;
		}
		Event event = seek_event_handler(el.getRequestPath().toLowerCase());
		if (event == null) {
			handle_404(el);
		} else {
			if (!event.enable) {
				
				el.text("Event for " + el.getRequestPath().toLowerCase() + " has been disabled. please check WEB-INF/lizar/disable_event.json");
				
				return;
			}
			try {
				_handle(event, el.getRequestPath(), el);
			} catch (Exception e) {
				e.printStackTrace();
				throw new Exception("request:" + el.getRequestPath() + " throw a exception in " + event.getClass().getName());
			}
		}
	}
	
	private void handle_404(EventLoader el) throws IOException, ServletException {
		String full_path = Config.path_filter("/WEB-INF/lizar/404.html", "");
		File _404 = new File(full_path);
		if (_404.isFile()) {
			StaticResource.handle_abs_file(el, full_path, HttpServletResponse.SC_NOT_FOUND);
		} else {
			full_path = Config.path_filter("/WEB-INF/lizar/404.html");
			if (!_404.exists()) {
				_404 = new File(full_path);
				FileTool.write_to_file("The event you are looking for is not exists, if you want to change the 404 content, pls replace the file /WEB-INF/lizar/404.html", _404, "utf-8");
			} else {
				FileTool.delete_sub_files(_404);
				_404.delete();
				_404 = new File(full_path);
				FileTool.write_to_file("The event you are looking for is not exists, if you want to change the 404 content, pls replace the file /WEB-INF/lizar/404.html", _404, "utf-8");
			}
			StaticResource.handle_abs_file(el, full_path, HttpServletResponse.SC_NOT_FOUND);
		}
		if (int_cpt != null)
			int_cpt.after(el);
	}
	
	private void handle_500(EventLoader el) throws IOException, ServletException {
		String full_path = Config.path_filter("/WEB-INF/lizar/500.html", "");
		File _500 = new File(full_path);
		if (_500.isFile()) {
			StaticResource.handle_abs_file(el, full_path, HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
		} else {
			if (!_500.exists()) {
				_500 = new File(full_path);
				FileTool.write_to_file("The event you visit get a server internal exception, if you want to change the 500 content, pls replace the file /WEB-INF/lizar/500.html", _500, "utf-8");
			} else {
				FileTool.delete_sub_files(_500);
				_500.delete();
				_500 = new File(full_path);
				FileTool.write_to_file("The event you visit get a server internal exception, if you want to change the 500 content, pls replace the file /WEB-INF/lizar/500.html", _500, "utf-8");
			}
			StaticResource.handle_abs_file(el, full_path, HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
		}
		if (int_cpt != null)
			int_cpt.after(el);
	}
	
	/**
	 * @param path such as /search /search.json /search/blog.json
	 */
	private Event seek_event_handler(String path) {
		if (path.contains("."))
			path = path.substring(0, path.indexOf("."));
		if (!StringHelper.equals(path, "/") && path.endsWith("/"))
			path = path.substring(0, path.length() - 1);
		Event e = null;
		e = minor_event_handler_map.get(path);
		if (e != null)
			return e;
		int level = visit_path_level(path);
		for (int l = 0; l < level; l++) {
			e = high_event_handler_map.get(path);
			if (e != null)
				return e;
			path = path.substring(0, path.lastIndexOf("/"));
		}
		e = high_event_handler_map.get("/");
		return e;
	}
	
	@Override
	public void born() throws Exception {
		log.info(this.getClass().getSimpleName() + " starting");
		if (started)
			throw new LifecycleException(this.getClass().getSimpleName() + " has started.");
		
		if (event_map == null)
			event_map = Web.events;
		high_event_handler_map = new HashMap<String, Event>();
		minor_event_handler_map = new HashMap<String, Event>();
		disable_list = new JList();
		log.info("..............................................................................");
		log.info("\t\t\tTotally " + event_map.size() + " Event is going to load.");
		log.info("..............................................................................");
		pre_handle();
		_init_tpl_vars();
		_initgetInt_cpt();
		_init_er();
		_load_template_support(Config.xpathgetStr("template.class"), Config.xpath_list("template.listen"), Config.xpath_entity("template.params"));
		ContentType.init();
		static_src = StaticResource.instance(Web.context.getRealPath(""));
		static_src.born();
		EventLoader.context = Web.context;
		reload_disable_map();
		
		started = true;
	}
	
	@Override
	public void die() throws LifecycleException {
		log.info(this.getClass().getSimpleName() + " stopping");
		if (!started)
			throw new LifecycleException(this.getClass().getSimpleName() + " has stopped.");
		
		static_src.die();
		started = false;
	}
	
	public static void main(String[] args) {
		System.out.println(visit_path_level("///"));
	}
	
	@Override
	public void init_property() throws Exception {
		// TODO Auto-generated method stub
		
	}
	
}
